VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
  Persistable = 0  'NotPersistable
  DataBindingBehavior = 0  'vbNone
  DataSourceBehavior  = 0  'vbNone
  MTSTransactionMode  = 0  'NotAnMTSObject
END
Attribute VB_Name = "clsLevel"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'====================================================================================
'clsLevel - A level stored in memory
'====================================================================================
Option Explicit

'--- Graphics -----------------------------------------------------------------------
Private Viewport As New ImageDC     'Buffer the level is drawn onto before screen

'--- Sprites ------------------------------------------------------------------------
Private Zim As New ISprite_Ghost    'Zim Ghost class
Private Dib As New ISprite_Ghost    'Dib Ghost class
Private Gir As New ISprite_Ghost    'Gir Ghost class
Private Kroc As New ISprite_Ghost   'Kroc Ghost class
Public LoveTimeout As Long          'Counter for how long to stay in edible mode
Public GhostMultiplier As Long      'Number of ghosts eaten in one bout

'The path finder is used to guide the ghosts back to the pen once eaten
Private PathFinder As New clsPathFinder

Public Mar As New ISprite_Mar       'Pac Mar, public so the ghosts see her position

'--- Level data ---------------------------------------------------------------------
Public Data As New clsLevelData     'Shared core level functionality
Public YScroll As Long              'Vertical scrolling offset in pixels
Public RemainingTime As Long        'Counter for number of seconds elapsed in play
Public Paused As Boolean            'True = Game is paused

'--- Stats --------------------------------------------------------------------------
Public StatsPointsEarnt As Long     'Number of points earnt this level alone
Public StatsGhostsEaten As Long     'Number of ghosts eaten the level alone
Public StatsTimeTaken As Long       'Time spent on level

'--- The Pickup ---------------------------------------------------------------------
Public PickupWaiting As Boolean     'If the pickup is on the map
Public PickupCountdown As Long      'Time remaining for pickup effect
Public Enum PM_PICKUPTYPE
    PT_NONE = 0
    
    PT_GOOD_FREELIFE = 1            'Extra life
    PT_GOOD_FREEHEART = 2           'Free power heart
    PT_GOOD_TURBO = 3               'Mar speed up
    PT_GOOD_MULTIPLIER = 4          '2x Points multiplier
    PT_GOOD_GHOSTSEAT = 5           'Ghosts eat cookies for you
    
    PT_BAD_SQUITS = 100             'Mar drops cookies behind her
    PT_BAD_REVERSE = 101            'Reverse controls!
End Enum
Public PickupType As PM_PICKUPTYPE

'====================================================================================
'CLASS Initialize
'====================================================================================
Private Sub Class_Initialize()
    'Setup the size of the buffer the level is drawn on to
    Viewport.Create 640, 416
End Sub

'====================================================================================
'Animate
'====================================================================================
Public Sub Animate()
    'The level data is loaded during `SM_Loading`, so ghosts cannot be animated _
     until after that as they refer to the level data
    If ScreenMode >= SM_Intro Then
        'Animate Mar and Move her
        Mar.Animate
        'Animate the ghosts and move them
        Kroc.Animate
        Dib.Animate
        Zim.Animate
        Gir.Animate
    End If

    Static Wait As Long
    If ScreenMode = SM_Load Then  '--------------------------------------------------
        'Before gameplay starts, open the doors and display the level
        Select Case ModeStep
            Case 0
                Wait = 0
                If Game.IsSingleLevel = False Then
                    'If the player is playing the full game, display the level number.
                    Status.SetMessageTop Project.LevelSequence(Game.LevelNumber).Title1, SLIDEUP, 44, True, SLIDEOUTDOWN
                Else
                    'If the player is playing a single level file, show the round number
                    Status.SetMessageTop "=== Round " & Game.Round & " ===", SLIDEUP, 25, True, SLIDEOUTDOWN
                End If
                ModeStep = 1

            Case 1
                Wait = Wait + 1
                If Wait = 4 Then
                    If Game.IsSingleLevel = False Then
                        Status.SetMessageBottom Project.LevelSequence(Game.LevelNumber).Title2, SLIDEDOWN, 35, True, SLIDEOUTUP
                    End If
                    ModeStep = 2
                End If

            Case 2
                Me.StatsGhostsEaten = 0
                Me.StatsPointsEarnt = 0
                Me.StatsTimeTaken = 0
            
                'Load the level
                Data.Load Project.LevelSequence(Game.LevelNumber).LevelFile, _
                          Project.LevelSequence(Game.LevelNumber).LevelTheme, _
                          Project.LevelSequence(Game.LevelNumber).DiffLevel

                'Initialise the Path Finder, used to direct the ghosts to the pen
                Set PathFinder.Maze = Data
                
                'Reset level variables
                p_ResetLevel
                
                'Remaining time does not reset each time you die, only when the level loads,
                '(or when time runs out, which causes a time-up death)
                RemainingTime = DiffModes(Data.Difficulty).TimeLimit
                
                'Loading complete, begin main gameplay
                ScreenMode = SM_Intro: Wait = 0
        End Select

    ElseIf ScreenMode = SM_Intro Then '----------------------------------------------
        'Wait until the message at the bottom has gone before continuing
        If Status.IsTopAnimationFinished = True Then
            'Open the doors, and once open go onto the next step
            If Status.OpenDoors(STATUS_SPEED_STANDARD) Then Me.StartGameplay
        End If
        
    'If the game is underway then...
    ElseIf ScreenMode = SM_Play Then '-----------------------------------------------
        'If in love (edible mode), count down time left
        If LoveTimeout > 0 Then
            LoveTimeout = (LoveTimeout - 1)
            'If the timer has run out, put the ghosts back into normal mode
            If LoveTimeout <= 0 Then
                If (Kroc.InLove = True) And (Kroc.Eaten = False) Then Kroc.InLove = False
                If (Dib.InLove = True) And (Dib.Eaten = False) Then Dib.InLove = False
                If (Zim.InLove = True) And (Zim.Eaten = False) Then Zim.InLove = False
                If (Gir.InLove = True) And (Gir.Eaten = False) Then Gir.InLove = False
                'Set the number of ghosts eaten during edible mode to 0
                Level.GhostMultiplier = 0
                'Is this the last heart, after all cookies have been eaten?
                If Data.CookieCount = 0 And Data.HeartCount = 0 Then
                    'Level complete
                    p_LevelComplete
                Else
                    'If this was the last heart, show a message
                    If Data.HeartCount = 0 Then Status.SetMessageBottom "Aww, no more loving :'(", SLIDEDOWN, 75, False

                End If
            End If
        End If
        
        If Me.PickupCountdown = 0 Or Me.PickupType <> PT_GOOD_TURBO Then
            If SharedAPI.GetKeyState(VK_SHIFT) And &H8000 Then
                'Dib is always the fastest ghost, match his speed when slowing down
                Mar.Speed = Project.DiffModes(Data.Difficulty).DibSpeed
            Else
                Mar.Speed = Project.DiffModes(Data.Difficulty).MarSpeed
            End If
        End If
        
        Static IsInPen As Boolean
        Dim Pos As PM_XY
        Pos.X = GetTileFromPx(Mar.PositionX + HalfTileSize)
        Pos.Y = GetTileFromPx(Mar.PositionY + HalfTileSize)
        'If Mar is in the pen, it's a safety zone
        If (Pos.Y = Data.PenY) And _
           (Pos.X = Data.PenX Or Pos.X = Data.PenX + 1) _
        Then
            If IsInPen = False Then
                'If any ghosts are eaten and on their way back, it's a trap!
                If (Zim.Eaten) Or (Dib.Eaten) Or (Kroc.Eaten) Or (Gir.Eaten) Then
                    Status.SetMessageTop "*** Watch Out! ***", SLIDEUP, 25, True
                    Status.SetMessageBottom "*** Watch Out! ***", SLIDEDOWN, 25, True
                Else
                    Status.SetMessageTop "Safety Zone!", SLIDEUP, 33, True
                    Status.SetMessageBottom "Safety Zone!", SLIDEDOWN, 33, True
                End If
                IsInPen = True
            End If
        Else
            If IsInPen = True Then IsInPen = False
        End If

    ElseIf ScreenMode = SM_Dead Then '-----------------------------------------------
        'If Mar has lost a life (play death anim)
        Select Case ModeStep
            Case 2
                If Status.IsBottomAnimationFinished = True Then
                    'Close the doors quickly
                    If Status.CloseDoors(STATUS_SPEED_QUICK) Then ModeStep = 3
                End If

            Case 3
                'If died after eating everything, and heart was still in effect...
                If Data.HeartCount = 0 And Data.CookieCount = 0 Then
                    p_LevelComplete
                Else
                    'Reset player and ghost positions
                    p_ResetLevel
                    ModeStep = 4
                End If

            Case 4
                'Open the doors quickly
                If Status.OpenDoors(STATUS_SPEED_QUICK) Then StartGameplay

        End Select
    End If
End Sub

Public Sub Render(ByVal hDC As Long)
    Dim TileXOffset As Long, TileYOffset As Long
    Dim X As Long, Y As Long

    'Can't render until the level is loaded
    If ScreenMode = SM_Load Then Exit Sub

    'Draw on the visible section of the level background (pre-rendered at load)
    Data.DrawViewport Viewport.hDC, 0, 0, 640, 416, TileSize, Me.YScroll

    Dim TileY As Long
    TileY = GetTileFromPx(YScroll)

    For Y = TileY To SharedMath.Range(TileY + 7, 0, Data.Height - 1)
        For X = 1 To SharedMath.Range(10, 0, Data.Width - 1)
            TileXOffset = (X * TileSize) - TileSize
            TileYOffset = ((Y - TileY) * TileSize) - (YScroll Mod TileSize)
            'Cookies!
            If Data.IfItem(X, Y, PM_COOKIE_TOP) Then Sprites.Cookie.DrawEx Viewport.hDC, TileXOffset + 24, TileYOffset - 8, 16, 16, 16, 0
            If Data.IfItem(X, Y, PM_COOKIE_LEFT) Then Sprites.Cookie.DrawEx Viewport.hDC, TileXOffset - 8, TileYOffset + 24, 16, 16, 16, 0
            If Data.IfItem(X, Y, PM_COOKIE_CENTER, (PM_COOKIE_CENTER Or PM_HEART)) Then Sprites.Cookie.DrawEx Viewport.hDC, TileXOffset + 24, TileYOffset + 24, 16, 16, 0, 0 'different cookies for even/odd
            'Heart (Power Pill)
            If Data.IfItem(X, Y, PM_HEART) Then
                'Draw the heart, but add a bounce to it using a sine-wave
                Sprites.Heart.Draw Viewport.hDC, TileXOffset + 16, _
                                   (TileYOffset + 16) + (Sin((Timer + X) * 4) * 2)
            End If

            'Draw random pie
            If X = (Data.PenX + 1) And Y = (Data.PenY - 1) And _
               Me.PickupWaiting = True _
            Then
                Sprites.Pie.Draw Viewport.hDC, TileXOffset - HalfTileSize, TileYOffset
                If (Frame.Count Mod 3) = 0 Then Sprites.Pie.Animate
            End If
        Next X
    Next Y

    'Add the sprites on top (in smallest to largest order)
    Gir.Draw Viewport.hDC
    Zim.Draw Viewport.hDC
    Kroc.Draw Viewport.hDC
    Dib.Draw Viewport.hDC

    'Mar is drawn last so that she appears above the ghosts, so she may eat them. <3
    Mar.Draw Viewport.hDC

    'Set the Offset for the particles so that they are moved according to the scroll
    GameMedia.Particles.OffsetX = TileSize: GameMedia.Particles.OffsetY = (YScroll - HalfTileSize)

    'Copy the level onto the screen buffer
    Viewport.Draw hDC, 0, 32
End Sub

'====================================================================================
'GetDirectionToPen: Choose the optimal direction to go to reach the pen
'====================================================================================
Public Function GetDirectionToPen(ByVal StartX As Long, ByVal StartY As Long, ByVal EndX As Long, ByVal EndY As Long, Optional ByVal GhostDirection As PM_DIRECTION = 0) As PM_DIRECTION
    Dim K As Long
    Dim PathLength(1) As Long, Direction(1) As PM_DIRECTION
    Dim IsEndReached As Boolean, Vec As PM_XY
    For K = 0 To 1
        PathFinder.PrepareSearch StartX, StartY, Data.PenX + K, Data.PenY, GetReverseDirection(GhostDirection)
        IsEndReached = False
        Do Until PathFinder.NextEmptyNode Or IsEndReached
            IsEndReached = PathFinder.FindNextNode
        Loop
        'Get the length of this path
        PathLength(K) = PathFinder.GenerateResultPath
        'Get the direction of the first step
        Vec.X = StartX: Vec.Y = StartY
        PathFinder.NextResultNode Vec.X, Vec.Y
        Vec.X = Vec.X - StartX: Vec.Y = Vec.Y - StartY
        Direction(K) = GetDirectionFromVector(Vec)
    Next K
    Dim Choice As Long
    'Is the left path shorter?
    If PathLength(0) < PathLength(1) Then
        Choice = 0
    ElseIf PathLength(1) < PathLength(0) Then
        Choice = 1
    ElseIf PathLength(0) = PathLength(1) Then
        Choice = Abs(StartX > Data.PenX)
    End If
    GetDirectionToPen = Direction(Choice)
End Function

'====================================================================================
'MoveSprite: Move a sprite in the level within the constraints of the maze
'====================================================================================
Public Sub MoveSprite(ByRef Sprite As ISprite)
    'This is a generic routine for moving sprites within the boundaries of the maze
    'Both Pac Mar and the Ghosts use this function to move themselves
    
    'Sprite.PositionX/Y gives us the location of the top left corner of the sprite
    'in pixels. Imagine that when moving left, that top left corner will cross the
    'boundary between tiles first, before the visible body of the sprite. However,
    'when moving right, the whole sprite will be over the next tile before the left-
    '-most edge moves over the boundry. This means then when moving Right/Down, the
    'sprite has moved too far to make a decision about what direction to move.
    
    'In order to prevent this, the decision is made when the center of the sprite
    'crosses the boundry.
    
    'Create a vector for the Sprite's direction, returns -1 for X/Y Left/Up
    'and +1 for X/Y Right/Down.
    Dim Vec As PM_XY
    Vec = GetVectorFromDirection(Sprite.Direction)
    Vec.X = (Vec.X * Sprite.Speed)
    Vec.Y = (Vec.Y * Sprite.Speed)
    
    'Get the tile that the sprite is currently on in the level (using top-left pixel)
    Dim Tile As PM_XY
    Tile.X = GetTileFromPx(Sprite.XPosition)
    Tile.Y = GetTileFromPx(Sprite.YPosition)
    
    'Get the tile that the sprite will be on, after moving (using top-left pixel)
    Dim Dest As PM_XY
    'If travelling down or right make judgement based on the farthest edge
    Dest.X = GetTileFromPx(Sprite.XPosition + Vec.X)
    Dest.Y = GetTileFromPx(Sprite.YPosition + Vec.Y)
    
    'Return the centre point of the sprite as a pixel location
    Dim Mid As PM_XY
    Mid.X = (Sprite.XPosition + HalfTileSize)
    Mid.Y = (Sprite.YPosition + HalfTileSize)
    
    'The position of the sprite after movement (based on the centre point of sprite)
    Dim NewX As Long, NewY As Long
    NewX = GetTileFromPx(Mid.X + Vec.X)
    NewY = GetTileFromPx(Mid.Y + Vec.Y)
    Mid.X = GetTileFromPx(Mid.X)  'Convert to tile position
    Mid.Y = GetTileFromPx(Mid.Y)
    
    Dim R1 As PM_XY, R2 As PM_XY
    R1.X = GetTileFromPx((Sprite.XPosition + TileSize) - 1)
    R1.Y = GetTileFromPx((Sprite.YPosition + TileSize) - 1)
    R2.X = GetTileFromPx((Sprite.XPosition + TileSize + Vec.X) - 1)
    R2.Y = GetTileFromPx((Sprite.YPosition + TileSize + Vec.Y) - 1)
    
    'Move if a) There's no wall in the way and sprite is not yet moving,
    '     or b) If you're already in motion and need to continue
    If (Sprite.IsMoving = False And Data.IfNotItem(Tile.X, Tile.Y, Sprite.Direction)) Or _
       (Sprite.IsMoving = True) _
    Then
        'If a), start moving / keep moving
        Sprite.IsMoving = True
        
        '!//If R2.Y = 12 And (Sprite.YPosition Mod TileSize) < 4 And Sprite.Direction = DIR_DOWN Then Stop
        
        'If the next movement will: -------------------------------------------------
        'a.) Overshoot a junction...
        If ((Dest.X <> Tile.X) Or (Dest.Y <> Tile.Y)) And (Sprite.Pending > 0) And Data.IfNotItem(Mid.X, Mid.Y, Sprite.Pending) Then
            'Move sprite up to the wall / Junction, instead of overshooting
            Sprite.XPosition = (Mid.X * TileSize)
            Sprite.YPosition = (Mid.Y * TileSize)
            Sprite.Direction = Sprite.Pending
            Sprite.Pending = 0
        
        'or b.) Overshoot a wall...
        ElseIf ((Dest.X <> Tile.X) Or (Dest.Y <> Tile.Y)) And _
               Data.IfItem(Mid.X, Mid.Y, Sprite.Direction) _
        Then
            'Move sprite up to the wall / Junction, instead of overshooting
            Sprite.XPosition = (Mid.X * TileSize)
            Sprite.YPosition = (Mid.Y * TileSize)
            Sprite.IsMoving = False
            Sprite.Pending = 0
        
        Else
            If (R1.Y < R2.Y) And Data.IfItem(R1.X, R1.Y, Sprite.Direction) Then
                'Move sprite up to the wall / Junction, instead of overshooting
                Sprite.XPosition = (Mid.X * TileSize)
                Sprite.YPosition = (Mid.Y * TileSize)
                Sprite.IsMoving = False
                Sprite.Pending = 0
            Else
                'Move the sprite in its direction
                Sprite.XPosition = (Sprite.XPosition + Vec.X)
                Sprite.YPosition = (Sprite.YPosition + Vec.Y)
            End If
        End If
        
        'If the sprite's center has crossed the boundry from one tile to the next,
        'Make a decision about what direction to turn once the sprite has reached
        'the junction point later on.
        If (Mid.X <> NewX) Or (Mid.Y <> NewY) Then
            If Sprite.Pending = 0 Then Sprite.Pending = Sprite.MakeDecision(NewX, NewY)
        End If
        
        'Check if a sprite goes off the map -----------------------------------------
        If Sprite.XPosition >= (Data.Width - 1) * TileSize Then
            'If the Sprite walks off the right side of the Level, appear on the left
            Sprite.XPosition = 0
            
        ElseIf Sprite.XPosition <= 0 Then
            'If the Sprite walks off the left side of the Level, appear on the right
            Sprite.XPosition = (Data.Width - 1) * TileSize
        End If
        
    Else
        'The sprite has a wall in front of them and can no longer continue
        Sprite.IsMoving = False
    End If
    
End Sub

'====================================================================================
'StartGamplay: Reset all positions and then start playing
'====================================================================================
Public Sub StartGameplay()
    p_ResetLevel
    
    'Gameplay will start after "Ready", "Set", "Go!" - See Animate() above.
    ScreenMode = SM_Start
End Sub

'====================================================================================
'ResetLevel: Put everything back to original positions for a restart
'====================================================================================
Private Sub p_ResetLevel()
    'Stop any edible-mode already in action
    LoveTimeout = 0
    Level.GhostMultiplier = 0
    'Clear any pickup effects
    p_ApplyPickup Me.PickupType, False
    Me.PickupWaiting = False
    'Clear any messages at the bottom of the screen
    '?//Status.ClearMessageBottom
    
    'Position Mar in the starting place
    Mar.PositionX = (Data.MarX * TileSize) - 32
    Mar.PositionY = (Data.MarY * TileSize)
    Mar.Direction = DIR_LEFT: Mar.Pending = 0
    Mar.IsMoving = True
    Mar.Speed = DiffModes(Data.Difficulty).MarSpeed
    
    'Set the AI behaviours for the ghosts
    Kroc.GhostType = GT_Kroc
    Dib.GhostType = GT_Dib
    Zim.GhostType = GT_Zim
    Gir.GhostType = GT_Gir
    
    'Remove any particle effects left on screen
    GameMedia.Particles.ClearAll
    
    'Set the starting screen location (center around Pac Mar)
    YScroll = SharedMath.Range(Mar.PositionY - (3 * TileSize), TileSize, (Data.Height - 7) * TileSize)
    
    '?//p_ApplyPickup PT_GOOD_TURBO, True
End Sub

'====================================================================================
'EatCookies: Check if a sprite is over a cookie, and eat it if so
'====================================================================================
Public Sub EatCookies(ByRef Sprite As ISprite)
    'This function is in the level class because 1) VB6 cannot do inheritence, and
    '2) both Pac Mar can eat the cookies, as well as the ghosts when you get the
    '"ghosts eat cookies" power up. The .Animate() subs in ISprite_Ghost and
    'ISprite_PacMar will come here to check if they have eaten a cookie in their
    'current position
    
    Dim SpriteCentre As PM_XY, CellCentre As PM_XY
    'Get the centre point of the sprite on the maze, in pixels
    SpriteCentre.X = Sprite.XPosition + HalfTileSize
    SpriteCentre.Y = Sprite.YPosition + HalfTileSize
    'Relate that to the centre point of the cell the sprite is nearest to
    CellCentre.X = (TileSize * SharedMath.GetTileFromPx(SpriteCentre.X)) + HalfTileSize
    CellCentre.Y = (TileSize * SharedMath.GetTileFromPx(SpriteCentre.Y)) + HalfTileSize
    
    'Pac Mar is 19 pixels in radius, and the cookies are 8 pixels in diameter, that
    'means that if the distance between the centre point of the sprite, and the
    'centre point of the cookie is 11 or less pixels, then the cookie is wholly
    'within the sprite, and can be removed
    If SharedMath.GetPythagDistanceFromPoint(SpriteCentre.X, SpriteCentre.Y, CellCentre.X, CellCentre.Y) <= 11 Then
        'Remove the centre cookie
        EatCookie GetTileFromPx(SpriteCentre.X), GetTileFromPx(SpriteCentre.Y), PM_COOKIE_CENTER
        
    ElseIf SharedMath.GetPythagDistanceFromPoint(SpriteCentre.X, SpriteCentre.Y, CellCentre.X - 32, CellCentre.Y) <= 11 Then
        'Remove the left cookie (doubles as right cookie for adjacent cell)
        EatCookie GetTileFromPx(SpriteCentre.X), GetTileFromPx(SpriteCentre.Y), PM_COOKIE_LEFT
        
    ElseIf SharedMath.GetPythagDistanceFromPoint(SpriteCentre.X, SpriteCentre.Y, CellCentre.X, CellCentre.Y - 32) <= 11 Then
        'Remove the top cookie (doubles as the bottom cookie for adjacent cell)
        EatCookie GetTileFromPx(SpriteCentre.X), GetTileFromPx(SpriteCentre.Y), PM_COOKIE_TOP
    End If
End Sub

'====================================================================================
'EatCookie: Take a cookie off the level, and keep track of how many is left
'====================================================================================
Public Function EatCookie(ByVal X As Long, ByVal Y As Long, ByVal CookieNumber As PM_TILE) As Boolean
    'Ignore positions outside the map (caused when you go through the tunnel)
    If (X < 0) Or (X >= Data.Width) Then Exit Function
    
    'Is this cookie already removed
    If Data.IfNotItem(X, Y, CookieNumber) Then
        EatCookie = False
    Else
        'Make a crunch sound
        SFX.CookieCrunch.Play False 'change to true to prevent overlapping crunches
        
        'Remove the cookie from the level by setting its' bit to 1
        Data.RemoveItem X, Y, CookieNumber
        Game.TotalCookies = Game.TotalCookies + 1   'Total cookie count for the game
        IncreaseScore 10
        EatCookie = True
        
        'If 2x Multiplier is on, create a "x2" particle
        If Me.PickupType = PT_GOOD_MULTIPLIER Then
            Dim Position As PM_XY, TileOffset As PM_XY
            TileOffset.X = (X * TileSize)
            TileOffset.Y = (Y * TileSize)
            
            If IfNotBits(CookieNumber, PM_COOKIE_CENTER) Then
                Position.X = TileOffset.X + 27
                Position.Y = TileOffset.Y + 28
            ElseIf IfNotBits(CookieNumber, PM_COOKIE_LEFT) Then
                Position.X = TileOffset.X - 4
                Position.Y = TileOffset.Y + 28
            ElseIf IfNotBits(CookieNumber, PM_COOKIE_TOP) Then
                Position.X = TileOffset.X + 27
                Position.Y = TileOffset.Y - 4
            End If
            GameMedia.Particles.Create PT_X2, Position.X, Position.Y, 25
        End If
        
        'Show special messages as certain milestones
        If Data.CookieCount = 50 Then Status.SetMessageBottom "50 Cookies left!", SLIDECENTER, 64, False
        If Data.CookieCount = 20 Then Status.SetMessageBottom "20 Cookies left!", SLIDECENTER, 64, False
        If (Data.CookieCount = 0) And (Data.HeartCount = 0) And Me.LoveTimeout = 0 Then
            'When the player completes the level!
            p_LevelComplete
        End If
    End If
End Function

'====================================================================================
'EatHeart: Take a power pill off the level (verify that there is one to remove)
'====================================================================================
Public Function EatHeart(ByVal X As Long, ByVal Y As Long) As Boolean
    'Is this heart already removed
    If Data.IfNotItem(X, Y, PM_HEART) Then
        'No heart to remove
        EatHeart = False
    Else
        'Remove the heart, increase score
        Data.RemoveItem X, Y, PM_HEART
        
        'If 2x Multiplier, add 2x Particle
        If Me.PickupType = PT_GOOD_MULTIPLIER Then
            GameMedia.Particles.Create PT_X2, (X * TileSize) + HalfTileSize, _
                             ((Y * TileSize) + HalfTileSize) - (YScroll Mod TileSize), 25
        End If
    
        'Play the eat heart sound effect
        SFX.EatHeart.Play False
        
        'Apply the effects to the ghosts
        p_EatHeart
    End If
End Function

'====================================================================================
'PRIVATE EatHeart: Apply the power-pill effect
'====================================================================================
Private Sub p_EatHeart()
    'On hard difficulty level there is no time to eat ghosts
    If Project.DiffModes(Data.Difficulty).LoveTime > 0 Then
        'Set all the ghosts to in-love mode (only if they are not already
        'eaten). The `InLove` property set function in clsGhost automatically
        'handles reversing the ghost's directions and slowing them down
        If Kroc.Eaten = False Then Kroc.InLove = True
        If Dib.Eaten = False Then Dib.InLove = True
        If Zim.Eaten = False Then Zim.InLove = True
        If Gir.Eaten = False Then Gir.InLove = True
        
        'Set how many frames the effect will last for
        LoveTimeout = DiffModes(Data.Difficulty).LoveTime
        
        'Display message on the bottom of the screen
        Select Case Data.HeartCount
            Case 3: Status.SetMessageBottom "Come here my Precious!", SLIDEDOWN, 32, False
            Case 2: Status.SetMessageBottom "I feel so loved n_n", SLIDEDOWN, 32, False
            Case 1: Status.SetMessageBottom "Move it or lose it cuties!", SLIDEUP, 32, False
            Case 0: Status.SetMessageBottom ":SHAKEANIM", SHOWLEFT, 100, False
        End Select
    End If
End Sub

'====================================================================================
'EatPie: Eat the random pie on the level
'====================================================================================
Public Sub EatPie()
    Dim r As Long
    
    'Give 500 points
    Me.IncreaseScore 500
    
    'Ranodmly choose a powerup
    Randomize
    r = Int(Rnd(1) * 10) + 1
    r = 10
    Select Case r
        Case 1 To 2
            p_ApplyPickup PT_GOOD_FREEHEART, True
        Case 3 To 4
            p_ApplyPickup PT_GOOD_TURBO, True
        Case 5 To 6
            p_ApplyPickup PT_GOOD_MULTIPLIER, True
        Case 7 To 8
            p_ApplyPickup PT_GOOD_GHOSTSEAT, True
        Case 9 To 10
            p_ApplyPickup PT_BAD_REVERSE, True
    End Select
    Me.PickupWaiting = False
End Sub

'====================================================================================
'PRIVATE ApplyPickup: apply/remove the effects of a power up / down
'====================================================================================
Private Sub p_ApplyPickup(ByVal Pickup As PM_PICKUPTYPE, ByVal Enabled As Boolean)
    'Which power up / down to apply?
    Select Case Pickup
        '--- Power Ups --------------------------------------------------------------
        Case PM_PICKUPTYPE.PT_GOOD_FREELIFE
            'Award a free life
            If Enabled = True Then p_GiveLife
            
        Case PM_PICKUPTYPE.PT_GOOD_FREEHEART
            'Award a free power heart
            If Enabled = True Then p_EatHeart
            
        Case PM_PICKUPTYPE.PT_GOOD_TURBO
            'Turbo Mode for Pac Mar
            If Enabled = True Then
                'Increase her speed
                Mar.Speed = Range(Project.DiffModes(Data.Difficulty).MarSpeed + 1, 7, 8)
                Status.SetMessageBottom "=== TURBO ===", SLIDEUP, 48, True
                'Effect lasts for a few moments
                p_StartPickupCountdown Pickup
            Else
                'Return to normal speed
                Mar.Speed = DiffModes(Data.Difficulty).MarSpeed
            End If
        
        Case PM_PICKUPTYPE.PT_GOOD_MULTIPLIER
            '2x Points Multiplier
            If Enabled = True Then
                Status.SetMessageBottom "=== 2x Multiplier! ===", SLIDEUP, 48, True
                p_StartPickupCountdown Pickup
            End If
            
        Case PM_PICKUPTYPE.PT_GOOD_GHOSTSEAT
            'Ghosts eat cookies
            If Enabled = True Then
                Status.SetMessageBottom "=== Ghosts Eat Cookies! ===", SLIDEUP, 48, True
                p_StartPickupCountdown Pickup
            End If
            
        '--- Power Downs ------------------------------------------------------------
        Case PM_PICKUPTYPE.PT_BAD_REVERSE
            If Enabled = True Then
                Status.SetMessageBottom "=== REVERSE CONTROLS ===", SLIDEUP, 48, True
                p_StartPickupCountdown Pickup
            End If
            
    End Select
    'If a power up is being switched off, clear the remembered type
    If Enabled = False Then Me.PickupType = PT_NONE: Me.PickupCountdown = 0
End Sub

'====================================================================================
'PRIVATE StartPickupCountdown: Some power[up/down]s last for a short while
'====================================================================================
Private Sub p_StartPickupCountdown(ByVal Pickup As PM_PICKUPTYPE)
    'Set the number of seconds for the countdown from the difficulty level data
    Me.PickupCountdown = Project.DiffModes(Data.Difficulty).PickupTimeout
    'Remember which power up/down is in effect so it can be switched off afterwards
    Me.PickupType = Pickup
End Sub

'====================================================================================
'IncreaseScore: Simply add to the player's score
'====================================================================================
Public Sub IncreaseScore(ByVal Points As Long)
    'All this function does is increase the score by the given amount.
    'The reason a function is used instead of just using score = score + 10 where
    'needed is that we need to give an extra life every 10'000 points and I don't
    'want to check for 10'000 points in each place that increases score.
    Dim OldScore As Long
    OldScore = Game.Score
    
    'If the 2x Multiplier power up is in effect?
    If Me.PickupCountdown > 0 And Me.PickupType = PT_GOOD_MULTIPLIER Then
        Points = Points * 2
    End If
    
    Game.Score = Game.Score + Points                    'increase score for this game
    Me.StatsPointsEarnt = Me.StatsPointsEarnt + Points  'Score this level alone
    
    'Did the points roll over a 10'000 boundry?
    If (Game.Score Mod 10000) < (OldScore Mod 10000) Then
        p_GiveLife
    End If
End Sub

'====================================================================================
'PRIVATE GiveLife: +1up
'====================================================================================
Private Sub p_GiveLife()
    'Pac Mar can only have a maximum of 6 lives
    If Game.Lives < 6 Then
        'Give the player an extra life!
        Game.Lives = Game.Lives + 1
        'Add the +1Up particle
        GameMedia.Particles.Create PT_1UP, Mar.PositionX + 32, Mar.PositionY, 64
        'Show a message
        Status.SetMessageBottom "=== Extra Life! ===", SLIDECENTER, 100, True
    End If
End Sub

'====================================================================================
'LoseLife: Actions to do when Mar is hit
'====================================================================================
Public Sub LoseLife()
    'Remove powerups
    p_ApplyPickup PickupType, False
    
    'Change the Mar sprite
    Mar.Die
    'Create the skull and crossbones particle
    GameMedia.Particles.Create PT_DEATH, Mar.PositionX + 32, Mar.PositionY, 80
    
    'Remove a life
    Game.Lives = (Game.Lives - 1)
    'If no more lives left
    If (Game.Lives = 0) Then
        'Switch to Game Over screen (Animate() function above will handle this)
        ScreenMode = SM_GameOver
        Status.ClearMessageBottom: Status.ClearMessageTop
        
    Else
        'If death was due to running out of time, start the level time again
        If RemainingTime = 0 Then
            'Get the level time from the sequence
            RemainingTime = Project.DiffModes(Project.LevelSequence(Game.LevelNumber).DiffLevel).TimeLimit
            Status.SetMessageBottom "Out of Time", SLIDEUP, 64, True
            
        Else
            'Show the death message
            '!//Status.SetMessageBottom "x_x;", SLIDEDOWN, 40, True
        End If
        'Play death animation...
        ScreenMode = SM_Dead
        
    End If
End Sub

'====================================================================================
'PRIVATE LevelComplete: The level has been completed, proceed to the next
'====================================================================================
Private Sub p_LevelComplete()
    'Freeze gameplay, all sprites will stop moving
    Status.SetMessageBottom "w00t!", SLIDECENTER, 64, True
    'Level.Animate above will handle the rest
    ScreenMode = SM_Complete
End Sub

'====================================================================================
'HandleOneSecond: An event that happens once every second of real time
'====================================================================================
Public Sub HandleOneSecond()
    'If in play mode (paused time does not count)
    If ScreenMode = SM_Play Then
        'Remove one second ----------------------------------------------------------
        Me.RemainingTime = Me.RemainingTime - 1
        Me.StatsTimeTaken = Me.StatsTimeTaken + 1
        'Add one second to total game time
        Game.TotalTime = Game.TotalTime + 1
        'Give a warning with two minutes remaining
        If Me.RemainingTime = 120 Then Status.SetMessageBottom "SHAKE 'DAT ASS GIRL!", SLIDEDOWN, 64, False
        'Give a warning with one minute remaining
        If Me.RemainingTime = 60 Then Status.SetMessageBottom "HURRY UP I'M HUNGRY!", SLIDEUP, 64, False
        '30 Seconds warning
        If Me.RemainingTime = 30 Then Status.SetMessageBottom "MOVE IT OR LOSE IT!", SLIDECENTER, 64, True
        '10 seconds warning!
        If Me.RemainingTime < 10 Then
            Status.SetMessageBottom "*** " & Me.RemainingTime & " ***", SLIDEUP, 30, True, SLIDEOUTUP
        End If
        'Final time out
        If Me.RemainingTime = 0 Then Me.LoseLife: Exit Sub
        
        'Is a pickup due? -----------------------------------------------------------
        Dim IntervalTime As Long, WaitTime As Long, ModTime As Long
        'Get the time between pickups appearing
        IntervalTime = Project.DiffModes(Data.Difficulty).PickupInterval
        'The amount of time they wait before disappearing
        WaitTime = Project.DiffModes(Data.Difficulty).PickupWaitTime
        'The time remaining to the next interval period
        ModTime = Me.RemainingTime Mod IntervalTime
        
        'If a pickup is not currently on screen...
        If Me.PickupWaiting = False Then
            'If a power up/down is in effect...
            If Me.PickupCountdown > 0 Then
                Me.PickupCountdown = Me.PickupCountdown - 1
                If Me.PickupCountdown = 0 Then p_ApplyPickup Me.PickupType, False
                
            'If it's time to show a pickup, then do
            ElseIf ModTime = 0 And (Me.RemainingTime <> Project.DiffModes(Data.Difficulty).TimeLimit) Then
                Me.PickupWaiting = True
                Status.SetMessageBottom "Hmmm, Pie!", SLIDEDOWN, 32, True
            End If

        Else
            'A pickup is already on screen, has it waited long enough?
            If ModTime Mod WaitTime = 0 Then
                Me.PickupWaiting = False
            End If
        End If
        
        'If a powerup/down is in effect? --------------------------------------------
        '!//Stop
    End If
End Sub

'====================================================================================
'CLASS Terminate - When the program exits, free up the memory in this class
'====================================================================================
Private Sub Class_Terminate()
    'Clear the level buffer image
    Set Viewport = Nothing
    
    'Get rid of Mar and the ghosts
    Set Mar = Nothing
    Set Kroc = Nothing
    Set Dib = Nothing
    Set Zim = Nothing
    Set Gir = Nothing
End Sub

'=== END ============================================================================
'This work is licenced under the Creative Commons Attribution 2.5 License
